Skip to content

[PoC] feat: agent plugin#217

Draft
MarioCadenas wants to merge 12 commits intomainfrom
agent-plugin
Draft

[PoC] feat: agent plugin#217
MarioCadenas wants to merge 12 commits intomainfrom
agent-plugin

Conversation

@MarioCadenas
Copy link
Copy Markdown
Collaborator

@MarioCadenas MarioCadenas commented Mar 26, 2026

Summary

Add a framework-agnostic AI agent plugin to AppKit with auto-tool-discovery from existing plugins, a Databricks Model Serving adapter, three framework adapters (Databricks, Vercel AI, LangChain), and a createAgent() convenience API.

Main changes

Agent plugin core (packages/appkit/src/plugins/agent/):
New deferred-phase plugin that scans all registered plugins for ToolProvider implementations, collects their tools with pluginName.toolName namespacing, and exposes 7 HTTP routes (chat via SSE, cancel, threads CRUD, tools list, agents list). Manages in-memory threads per userId and supports multiple named agents with a configurable default. The chat handler builds an executeTool closure over the request for user-context-aware tool execution.

DatabricksAdapter (packages/appkit/src/agents/databricks.ts):
Adapter that talks directly to Databricks Model Serving /invocations endpoints using raw fetch(). Authenticates per-request via an authenticate() callback (no stale tokens), sanitizes tool names (dots to double-underscores) for the Databricks API, and handles both structured tool_calls responses and text-based tool call fallback parsing (Llama JSON format and Python-style). Includes a fromServingEndpoint() async factory and a createDatabricksModel() helper for Vercel AI SDK integration.

ToolProvider on existing plugins (packages/appkit/src/plugins/{analytics,files,genie,lakebase}/):
All four existing plugins now implement the ToolProvider interface with getAgentTools() and executeAgentTool(). Analytics exposes query (1 tool), Files generates per-volume tools (6 per volume), Genie generates per-space tools (2 per space), and Lakebase exposes query (1 tool).

createAgent() wrapper (packages/appkit/src/core/create-agent.ts):
Higher-level API that wraps createApp with server() and agent() pre-configured. Supports single-agent shorthand (adapter) or multi-agent (agents record), flat server config (port, host), and returns an AgentHandle with registerAgent, getTools, getThreads, and plugins namespace.

Supporting infrastructure

  • packages/shared/src/agent.ts — Core types: ToolProvider, AgentToolDefinition, AgentAdapter, AgentEvent (7-event SSE protocol), Message, Thread, ThreadStore.
  • packages/appkit/src/agents/vercel-ai.ts — Adapter bridging Vercel AI SDK's streamText().fullStream to AgentEvent.
  • packages/appkit/src/agents/langchain.ts — Adapter bridging LangChain's streamEvents v2 to AgentEvent, with JSON Schema to Zod converter.
  • packages/appkit/src/plugins/agent/thread-store.tsInMemoryThreadStore implementation (Map-based, userId-scoped).
  • apps/agent-app/ — Standalone agent app project using createAgent(), with chat UI, markdown rendering, event stream panel, and theme selector.
  • apps/dev-playground/client/src/routes/agent.route.tsx — Agent chat page in dev-playground with inline autocomplete and event stream sidebar.

Examples

2 ways of building agents

in app:

import {
  agent,
  analytics,
  createApp,
  files,
  genie,
  server,
} from "@databricks/appkit";

createApp({
  plugins: [
    server(),
    analytics({}),
    genie({
      spaces: { demo: process.env.DATABRICKS_GENIE_SPACE_ID ?? "placeholder" },
    }),
    files(),
    agent({
      agents: {
        assistant: DatabricksAdapter.fromServingEndpoint({
          workspaceClient: wsClient,
          endpointName,
          systemPrompt:
            "You are a helpful data assistant. Use the available tools to query data and help users with their analysis.",
        }),
        autocomplete: DatabricksAdapter.fromServingEndpoint({
          workspaceClient: wsClient,
          endpointName: "databricks-gemini-3-1-flash-lite",
          systemPrompt: [
            "You are an autocomplete engine.",
            "The user will give you the beginning of a sentence or paragraph.",
            "Continue the text naturally, as if you are the same author.",
            "Do NOT repeat the input. Only output the continuation.",
            "Do NOT use tools. Do NOT explain. Just write the next words.",
          ].join(" "),
          maxSteps: 1,
        }),
      },
      defaultAgent: "assistant",
    }),
  ],
});
Screen.Recording.2026-03-26.at.18.17.40.mov

dedicated agent:

import { analytics, createAgent, files } from "@databricks/appkit";
import { DatabricksAdapter } from "@databricks/appkit/agents/databricks";

createAgent({
  plugins: [analytics(), files()],
  adapter: DatabricksAdapter.fromServingEndpoint({
    workspaceClient: new WorkspaceClient({}),
    endpointName,
    systemPrompt:
      "You are a helpful data assistant. Use the available tools to query data and help users with their analysis.",
  }),
  port: 8003,
})
Screen.Recording.2026-03-26.at.18.20.03.mov

@MarioCadenas MarioCadenas changed the title feat: agent plugin [PoC] feat: agent plugin Mar 26, 2026
Add ResponseStreamEvent types alongside internal AgentEvent. The adapter
contract (AgentEvent) stays unchanged; a new AgentEventTranslator converts
to Responses API SSE format at the HTTP boundary. Both /api/agent/chat
and future /invocations emit the same wire format.

- Add Responses API types to shared/agent.ts (self-contained, no openai dep)
- Add AgentEventTranslator with stateful sequence_number/output_index tracking
- AppKit extension events (appkit.thinking, appkit.metadata) preserved
- Update dev-playground and agent-app frontends to parse new SSE format
- Fix knip config for optional peer deps used by agent adapters

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…client

Add explicit tool types alongside ToolProvider auto-discovery:

- FunctionTool: user-defined tools with JSON Schema + execute callback
- HostedTool: Genie, VectorSearch, custom/external MCP server configs
- AppKitMcpClient: zero-dependency MCP client using raw fetch + JSON-RPC 2.0
- Discriminated ToolEntry union (source: plugin | function | mcp)
- collectTools() handles all three sources with conflict resolution
- addTools() for post-setup FunctionTool addition (HostedTools at setup only)
- MCP client lifecycle managed in setup/shutdown

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Wrap executeTool callback with this.execute() so all tool invocations
(ToolProvider plugins, FunctionTool, MCP) get uniform telemetry tracing
and 30s timeout via AppKit's interceptor chain. OBO-aware execution
via asUser(req) is preserved for ToolProvider tools.

Also fix main package exports for FunctionTool and HostedTool types.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Agent plugin accesses the server plugin from config.plugins (deferred phase)
and calls server.extend() to mount POST /invocations at the app root.

- Accepts Responses API request format ({ input, stream?, model? })
- Extracts user message from input array or string
- Delegates to the same internal chat handler flow
- No Plugin base class changes needed

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Add Zod schemas for both endpoint request formats:

- /api/agent/chat: chatRequestSchema (message, threadId?, agent?)
- /invocations: invocationsRequestSchema (input, stream?, model?)
- Structured 400 error responses with field-level details
- Replace manual type assertions with safe parsing

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Add getAgents() to plugin exports and comprehensive test coverage:

- event-translator tests: 13 tests covering all AgentEvent -> ResponseStreamEvent
  translations, sequence_number monotonicity, output_index tracking
- function-tool tests: 11 tests for isFunctionTool guard and
  functionToolToDefinition conversion (100% coverage)
- hosted-tools tests: 14 tests for isHostedTool guard and
  resolveHostedTools resolution for all 4 tool types (100% coverage)
- Total: 38 new tests, all passing (1392 total)

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
server.extend() throws when autoStart is true (by design for external
callers). The agent plugin now pushes directly into the server's
serverExtensions array for internal plugin-to-plugin coordination,
fixing the crash when using createAgent() which sets autoStart: true.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Two issues found via runtime debugging:

1. Plugin.execute() returns undefined on failure (production-safe).
   The executeTool callback now converts undefined to an error string
   so the adapter always has content for the tool message.

2. Large tool results (e.g. SHOW TABLES with 14K rows) exceeded the
   StreamManager 1MB event limit and blew up the model context.
   Tool results are now truncated at 50K chars with a notice.

Also guard against SSE events without a type field in both frontends.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>

# Conflicts:
#	pnpm-lock.yaml
Replace the GET /api/agent/tools and GET /api/agent/agents HTTP endpoints
with clientConfig(), which embeds the data into the HTML page at startup.
No round-trip needed — frontends read tools and agents synchronously via
getPluginClientConfig("agent") from @databricks/appkit-ui.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant